Skip to content

Fix imprecise types after assignment when strict-types=1 #3945

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Apr 19, 2025

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Apr 18, 2025

work with coecered type only when the assigned one is incompatible with the natively defined property type

closes phpstan/phpstan#12902

supercedes #3943

Comment on lines 52 to 77
self::$i = getInt();
assertType('float|int', self::$i); // could be int
assertNativeType('float|int', self::$i); // could be int

$this->impureCall();
assertType('float|int', self::$i);
assertNativeType('float|int', self::$i);
}
Copy link
Contributor Author

@staabm staabm Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the impureCall does not make the scope forget about already narrowed types of static property fetches, therefore we don't narrow in this scenario with this PR.

-> we only improve the instance-call case, because non-static fetches will be forgotten as the test in NarrowsNativeUnion proves

@staabm staabm marked this pull request as ready for review April 18, 2025 12:23
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

@ondrejmirtes
Copy link
Member

I've analyzed this situation. It's much more clear what's going on in this code sample: https://phpstan.org/r/903891e3-323a-494b-83a0-27aff806eb88

It's happening because IntegerType::toCoercedArgumentType() returns TypeCombinator::union($this, $this->toFloat()). And we take that, intrsection that with int|float and assign the value.

But what should happen first is even before we call toCoercedArgumentType, we should check whether the original value (ConstantIntegerType 1) is a subtype of the native property type.

  1. IntegerType->isSuperTypeOf(ConstantIntegerType) -> yes
  2. FloatType->isSuperTypeOf(ConstantIntegerType) -> no

Because 1) matches, we don't need to call toCoercedArgumentType at all. We simply assign the value as it is.

To get a list of types to compare, we can call TypeUtils::flattenTypes on the native property type. That way we can compare it in a foreach even when there's just a single type like int.

What's nice about this is that it can solve the problem for strict_types = 0 as well. The workflow should be:

  1. Go through the types, if we find one that matches, use the assigned expression.
  2. If we don't succeed with 1), check if we're in strict_types = 1. If yes, use the current logic in NodeScopeResolver with toCoercedArgumentType.

If done correctly, this will remove the need for #3943 as well.

TypeCombinator::intersect($assignedNativeType->toCoercedArgumentType(true), $propertyNativeType),
);
} else {
$scope = $scope->assignExpression($var, $assignedExprType, $assignedNativeType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gonna get executed if assigned type is compatible or if we're not in declare strict types. That's not what we want. We only want to execute this if assigned type is compatible.

Copy link
Contributor Author

@staabm staabm Apr 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so what to execute if not compatible and not strict types?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it works now as you imagined.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not compatible and not strict types, we currently can't do anything. Once we properly implement toCoercedArgumentType in the whole typesystem, then we can use it same way we use it now with strict_types=1.

Comment on lines +72 to +76
$this->impureCall();
assertType('int', self::$i); // should be float|int
assertNativeType('int', self::$i); // should be float|int
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a unrelated bug, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix in #3950

@@ -398,7 +398,7 @@ private static function isFileLintSkipped(string $file): bool

@fclose($f);

if (preg_match('~<?php\\s*\\/\\/\s*lint\s*([^\d\s]+)\s*([^\s]+)\s*~i', $firstLine, $m) === 1) {
if (preg_match('~<?php\\s*(?:declare\\s*\([^)]+\)\\s*;\\s*)?\\/\\/\s*lint\s*([^\d\s]+)\s*([^\s]+)\s*~i', $firstLine, $m) === 1) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't compatible with how php-parallel-lint works.

@@ -0,0 +1,88 @@
<?php declare(strict_types = 0); // lint >= 8.1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first line has to be <?php // lint >= 8.1 and the declare statement has to be on line 3.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks!

Comment on lines +72 to +76
$this->impureCall();
assertType('int', self::$i); // should be float|int
assertNativeType('int', self::$i); // should be float|int
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, probably.

@ondrejmirtes ondrejmirtes merged commit 67fb7a6 into phpstan:2.1.x Apr 19, 2025
416 of 417 checks passed
@ondrejmirtes
Copy link
Member

Thank you!

@staabm staabm deleted the bug12902 branch April 19, 2025 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

less precise type when strict-types=1 after assign
3 participants